Skip to content

πŸš€ release: v1.4.0-rc.1 β€” remote TLS upstreams, swarm/SecurityOpt confinement rails, 3 presets#106

Merged
scttbnsn merged 42 commits into
mainfrom
dev/v1.4
Jun 16, 2026
Merged

πŸš€ release: v1.4.0-rc.1 β€” remote TLS upstreams, swarm/SecurityOpt confinement rails, 3 presets#106
scttbnsn merged 42 commits into
mainfrom
dev/v1.4

Conversation

@scttbnsn

Copy link
Copy Markdown
Contributor

Starts the v1.4 release train: merges the dev/v1.4 soak branch into main so the v1.4.0-rc.1 tag can be cut from main (the established release flow β€” cf. #88 dev/v1.3 β†’ main). main stays at the v1.3.0 GA contract; this is the rc.1 cut for soak, not GA promotion (README "latest" prose flips at GA).

Headline changes (see CHANGELOG [1.4.0-rc.1])

  • Remote Docker TCP+TLS upstreams with active/passive failover (upstream.endpoints[]) β€” mTLS dialer across reverse-proxy, exec/attach hijack, and inspect side-channels; per-endpoint TLS + health probes.
  • Swarm ContainerSpec.Privileges seccomp/AppArmor confinement rails β€” completes parity with container-create.
  • SecurityOpt SELinux + system-paths directives are policy-evaluable (label=disable, label overrides, systempaths=unconfined incl. the MaskedPaths: [] client-side vector).
  • 3 new presets (portwing, portwing-with-exec, drydock-with-selfupdate) β†’ 15 total; runner preset load fix.
  • Alloc-free rate-limit token bucket; per-profile in-flight gauge cleanup on hot reload.
  • Dependency refresh folded via πŸ“¦ deps: fold npm-minor group into v1.4 soak (supersedes #102)Β #105 (sigstore-go 1.2.1, biome 2.5.0, fumadocs 16.10.3, tailwind 4.3.1, lucide 1.18, turbo 2.9.18).

No source version bump (build-injected). Chart stays at 1.3.0 for the rc per the soak convention (bumps at GA). After merge, v1.4.0-rc.1 gets tagged from main β†’ release-from-tag.yml builds + publishes ghcr/dockerhub/quay + cosign-signs the image.

scttbnsn and others added 30 commits June 11, 2026 08:24
Add three new request-body knobs (all default false β€” zero behavior change):
- deny_selinux_disable: blocks label=disable / label:disable SecurityOpt
- deny_selinux_label_override: blocks label=user:/role:/type:/level: overrides
- deny_unconfined_system_paths: blocks systempaths=unconfined SecurityOpt AND
  empty MaskedPaths/ReadonlyPaths arrays (direct-API bypass vector)

Wires the new fields through config.go, load.go SetDefaults, and
filter_options.go. Adds MaskedPaths/ReadonlyPaths *[]string pointer fields to
the container create wire type so nil (absent) and non-nil empty (unconfined)
are distinguishable. Covers all paths with table-driven unit tests, env-var
round-trip tests, config mapping tests, and fuzz seeds exercised against both
a permissive and a strict policy instance.
Add three opt-in knobs to request_body.service that gate swarm service
create/update on ContainerSpec.Privileges.Seccomp.Mode and AppArmor.Mode:

- deny_unconfined_seccomp: denies Mode=="unconfined" (EqualFold)
- deny_custom_seccomp_profiles: denies Mode=="custom" and also a Seccomp
  object with empty Mode but non-empty Profile blob (fail-closed)
- deny_unconfined_apparmor: denies AppArmor.Mode=="disabled"

All three default false (opt-in). Extends serviceContainerPrivileges with
Seccomp and AppArmor sub-structs decoded from the wire JSON. Adds three
SetDefault registrations in load.go so SOCKGUARD_* env vars are not
silently dropped by Viper. Maps all three fields in filter_options.go.

New tests: TestServiceInspectDenyUnconfinedSeccomp (7 cases),
TestServiceInspectDenyCustomSeccompProfiles (7 cases including
Profile-without-Mode fail-closed), TestServiceInspectDenyUnconfinedAppArmor
(6 cases), TestServiceInspectSeccompAndAppArmorRailsCompose,
TestServiceInspectSeccompDenialOnUpdatePath,
TestLoadHonorsServiceSeccompAppArmorEnvVars.
When hot reload drops a profile that had a concurrency cap, its
sockguard_inflight_requests series now disappears from the next scrape
rather than persisting at a stale value.

- Add Registry.DeleteInflightProfile to remove the in-flight gauge
  series for a named profile from the sync.Map exposition.
- In reloadCoordinator.reload, capture the old concurrency-capped
  profile set before activeCfg advances, swap the handler, then delete
  series for profiles absent from the new config (after Swap so new
  requests are already on the new chain).
- Add profileNamesWithConcurrency and removedProfiles helpers.
- Tests: series-absent-after-delete, sequential-recreate-at-zero,
  nil-safety, concurrent hammer under -race, coordinator integration
  (profile removed and profile retained across reload).
Replace atomic.Pointer[bucketState] with a single atomic.Uint64 that
packs the 16.16 fixed-point token count (bits [31:0]) and millisecond
timestamp mod 2^32 (bits [63:32]) into one word. Every admitted request
previously allocated a 16-byte bucketState heap object; the CAS now
stores directly into the uint64. Denied requests were already
allocation-free; both branches are now 0 allocs/op.

Before: BenchmarkLimiterAllowNHot β€” 47 ns/op, 16 B/op, 1 allocs/op
After:  BenchmarkLimiterAllowNHot β€” 36 ns/op,  0 B/op, 0 allocs/op
New:    BenchmarkBucket_AllowNPacked β€” 34 ns/op, 0 B/op, 0 allocs/op

Behavioral change: refill granularity drops from nanosecond to
millisecond precision. Sub-ms calls see elapsedMS=0 and skip refill.
For all supported rates (max 65535 t/s = ~1 token/15Β΅s), this is
negligible; documented in the bucket struct comment.

Config change: limits.rate.burst (and tokens_per_second, since burst
>= tps) is now capped at 65535 by the validator. The 16-bit integer
part of the packed token field overflows at 65536. Enforced at startup;
no operator config in the repo uses values near this bound.
…ceiling

The 1e9 'effectively unlimited' sentinels truncate in the 16.16 fixed-point
encoding (uint32(1e9*65536) wraps), silently corrupting token counts, so the
Limiter benchmarks now use MaxPackedBurst (65535). Benchmark comments now
state honestly that drained-bucket iterations measure the deny branch.
…confinement modes, gauge cleanup, packed bucket

- πŸ“ docs(config): SELinux label= / systempaths= knobs β€” example YAML, prose, knob table, env-var rows
- πŸ“ docs(config): service seccomp/AppArmor confinement-mode knobs β€” same four touchpoints
- πŸ“ docs(config): limits.rate.burst 65535 upper bound (packed token-field limit)
- πŸ“ docs(changelog): Added/Fixed/Changed entries for all four items
- πŸ“ docs(readme): roadmap β€” confinement parity, SecurityOpt evaluation, gauge cleanup, and alloc-free bucket marked shipped
… branches

During an rc soak, feature work lands on dev/vX.Y instead of main, but
ci-verify's pull_request trigger only covered main β€” dev-targeted PRs got
just the integration job and external checks. dev branches are the next
release line and deserve the same gate.
✨ v1.4 roadmap batch: SecurityOpt SELinux/systempaths, swarm confinement modes, gauge cleanup, alloc-free rate-limit bucket
Bumps the go-minor group in /app with 1 update: [github.com/sigstore/sigstore-go](https://github.com/sigstore/sigstore-go).


Updates `github.com/sigstore/sigstore-go` from 1.1.4 to 1.2.0
- [Release notes](https://github.com/sigstore/sigstore-go/releases)
- [Commits](sigstore/sigstore-go@v1.1.4...v1.2.0)

---
updated-dependencies:
- dependency-name: github.com/sigstore/sigstore-go
  dependency-version: 1.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps the actions-minor group with 1 update in the / directory: [github/codeql-action](https://github.com/github/codeql-action).


Updates `github/codeql-action` from 4.36.1 to 4.36.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@87557b9...8aad20d)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.36.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps the npm-minor group with 7 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.15.0` | `6.16.1` |
| [turbo](https://github.com/vercel/turborepo) | `2.9.16` | `2.9.17` |
| [next](https://github.com/vercel/next.js) | `16.2.7` | `16.2.9` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.1` | `25.9.2` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.16` | `19.2.17` |
| [fumadocs-mdx](https://github.com/fuma-nama/fumadocs) | `15.0.10` | `15.0.11` |
| [@types/mdx](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mdx) | `2.0.13` | `2.0.14` |



Updates `knip` from 6.15.0 to 6.16.1
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.16.1/packages/knip)

Updates `turbo` from 2.9.16 to 2.9.17
- [Release notes](https://github.com/vercel/turborepo/releases)
- [Changelog](https://github.com/vercel/turborepo/blob/main/RELEASE.md)
- [Commits](vercel/turborepo@v2.9.16...v2.9.17)

Updates `next` from 16.2.7 to 16.2.9
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](vercel/next.js@v16.2.7...v16.2.9)

Updates `@types/node` from 25.9.1 to 25.9.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/react` from 19.2.16 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `fumadocs-mdx` from 15.0.10 to 15.0.11
- [Release notes](https://github.com/fuma-nama/fumadocs/releases)
- [Commits](https://github.com/fuma-nama/fumadocs/compare/fumadocs-mdx@15.0.10...fumadocs-mdx@15.0.11)

Updates `@types/mdx` from 2.0.13 to 2.0.14
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mdx)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 6.16.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-minor
- dependency-name: turbo
  dependency-version: 2.9.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: next
  dependency-version: 16.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@types/node"
  dependency-version: 25.9.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: fumadocs-mdx
  dependency-version: 15.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-minor
- dependency-name: "@types/mdx"
  dependency-version: 2.0.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
…+ lockfile regen

The root overrides pinning next's nested postcss to ^8.5.14 were never
actually applied: the override spec must exactly match the workspaces'
direct dependency spec (^8.5.15), and the stale resolution survived in
package-lock.json/node_modules through incremental installs. Aligning
the spec and regenerating the lockfile from a clean tree drops the last
postcss@8.4.31 (SNYK-JS-POSTCSS-16189065, medium XSS); snyk test is now
clean across all projects.
…s for standalone resolvers

Snyk's SCM PR check resolves each changed manifest in isolation, where
root-level npm overrides are invisible β€” docs/package.json and
website/package.json standalone still resolved next's nested
postcss@8.4.31 (SNYK-JS-POSTCSS-16189065) even though the installed
workspace tree is clean. Mirroring the override into both workspace
manifests fixes the standalone view; npm ignores non-root overrides in
the real workspace install, so package-lock.json is unchanged.
…never installed)

Snyk's PR check resolves workspace manifests standalone, without root
lockfile or npm-overrides context, and does not apply the overrides
declared in the manifests themselves β€” so it keeps reporting next's
pinned postcss@8.4.31 even though the installed tree and lockfile only
ever contain >=8.5.15. Time-boxed ignore (expires 2026-09-15) at root
plus per-workspace mirrors, since manifest-only projects read policy
from the manifest's directory.
πŸ“¦ deps: batch Dependabot minors β€” sigstore-go 1.2.0, codeql-action 4.36.2, npm-minor group
Adds app/configs/lookout.yaml β€” a first-class sockguard preset for the
lookout Docker agent. Derived from the drydock preset: same rule surface
(container list/inspect/stats/lifecycle/create, image pull/inspect/remove,
events, narrow network/volume/distribution/service reads) with redaction
posture matched to the drydock passthrough topology
(redact_mount_paths=false, redact_container_env=false,
redact_network_topology=false). Includes allowed_runtimes: [runc] to
prevent 403s when lookout recreates containers from inspect specs.
Header comments document when standalone operators should re-enable
redactions and how to switch to lookout-with-exec.yaml for exec support.

Also adds examples/compose/lookout/ following the existing compose-example
convention: sockguard + lookout services sharing a named-volume unix socket,
self-contained sockguard.yaml (copy of the preset), and a README covering
audience, security tradeoffs, and the redaction note.
Extends lookout.yaml with exec support for interactive terminal access
through the lookout agent (e.g. terminal-over-websocket or drydock-driven
exec sessions in lookout's edge mode).

Adds rules for POST /containers/{id}/exec, POST /exec/{id}/start,
POST /exec/{id}/resize, and GET /exec/{id}/json following the exec-enabled
preset pattern (github-actions-runner.yaml syntax). Body inspection:
allow_privileged=false, allow_root_user=true for interactive sessions.

Header comments document when to use this preset vs. lookout.yaml (exec
disabled baseline) and when to use allowed_commands instead for pinned
argv use cases.
Extends drydock.yaml with the exec paths required for drydock's self-update
finalize flow. The flow (SelfUpdateController.runFinalizeCallbackInContainer)
uses POST /containers/{id}/exec to create an exec instance, POST /exec/{id}/start
to run it (Detach:false, Tty:false), and GET /exec/{id}/json to confirm the
exit code.

The exec body inspection uses allowed_commands to pin the permitted argv to
the single finalize entrypoint β€” any other exec call is denied at the body
inspection layer even though the rule allows POST /containers/*/exec.
allow_privileged=false, allow_root_user=false (drydock images run as nonroot).

Header comment documents the full self-update exec flow with source references,
the security tradeoff (narrower exec surface vs. full self-update support),
and the allowed_bind_mounts requirement for the helper container's socket bind.
…lfupdate

Updates all three registration touchpoints identified in the recon map:

README.md:
- Recent updates bullet: 12 β†’ 15, enumerate lookout + lookout-with-exec
  + drydock-with-selfupdate
- Features table YAML Configuration cell: 12 β†’ 15, add lookout to examples
- Bundled presets (12) β†’ (15) heading + dot-separated link list: add three
  new preset links
- Ready-to-run compose examples: add lookout link

docs/content/docs/presets.mdx:
- description frontmatter: add all three new preset names
- Add four new H2 sections following the existing pattern:
  Drydock with Self-Update, Lookout, Lookout with Exec
… lookout preset

lookout's GetContainerLogs() calls GET /containers/{id}/logs (including
follow=1 streaming). The preset previously omitted this path, returning
403 at runtime for any log request. sockguard's startup validator also
requires insecure_allow_read_exfiltration: true when a config allows the
logs endpoint β€” without it the server refuses to start.

- Add GET /containers/*/logs rule to app/configs/lookout.yaml
- Set insecure_allow_read_exfiltration: true with security-tradeoff comment
- Mirror both changes to examples/compose/lookout/sockguard.yaml
- Update header comment to reflect that logs are now allowed
The compose example set LOOKOUT_DOCKER_SOCKET which is not a variable
lookout reads. lookout reads DOCKER_SOCKET (confirmed in
internal/config/config.go:143). With the wrong var name lookout fell back
to auto-detection, found no socket (raw /var/run/docker.sock is not
mounted), and failed to connect to sockguard at runtime.
…exec preset

sockguard's startup validator (bodyInspectionConfiguredForEndpoint) only
considers exec body-inspected when AllowedCommands is non-empty. With
allow_root_user: true and no AllowedCommands the server refused to start:
"rules allow body-sensitive write endpoints without request body inspection".

- Add insecure_allow_body_blind_writes: true with security comment explaining
  the interactive-session tradeoff vs. the pinned-command pattern
- Add insecure_allow_read_exfiltration: true since the exec preset should
  also support lookout's GetContainerLogs() like the base lookout preset
- Add GET /containers/*/logs rule to match the base preset
- Operator guidance: use drydock-with-selfupdate.yaml pattern (allowed_commands
  list) when the command set is known and pinnable
…nd gitlab-runner

Both presets allowed exec endpoints without AllowedCommands and allowed
GET /containers/*/logs and POST /containers/*/attach without the
insecure_allow_read_exfiltration flag. sockguard's BuildChain validator
would refuse to start the server with either config.

- Add insecure_allow_body_blind_writes: true β€” exec needs arbitrary job
  step commands, not a pinned allowed_commands list
- Add insecure_allow_read_exfiltration: true β€” runner streams job output
  via logs and attach APIs
- Add explanatory comments referencing the socket-access mitigation
…eAndCompileRules coverage

TestPresetConfigsValidate only ran config.Load + config.Validate, which
does not catch missing insecure_allow_* flags that cause the server to
refuse startup (BuildChain = validateAndCompileRules β†’ compileClientProfiles).

The new test loads every preset from app/configs/ and runs
validateAndCompileRules on each one β€” the same path the live server takes
on startup. This closes the gap that let lookout-with-exec.yaml,
github-actions-runner.yaml, and gitlab-runner.yaml ship broken.
✨ feat(presets): lookout presets + drydock self-update variant (M2/M3/M7)
The lookout, lookout-with-exec, github-actions-runner, and gitlab-runner
presets allow GET /containers/*/logs and set insecure_allow_* flags, but
the docs still listed logs as denied and omitted the flags. A doc↔code
audit surfaced 12 discrepancies, each verified against the YAML rules.

- πŸ“ docs(lookout): move logs from Deniesβ†’Allows; disclose insecure_allow_read_exfiltration
- πŸ“ docs(lookout-with-exec): disclose insecure_allow_body_blind_writes + inherited exfil flag
- πŸ“ docs(runners): fix misleading "raw export" wording (logs/attach now stream); disclose both insecure flags
- πŸ› fix(examples): correct lookout README + compose header that claimed "no insecure_allow_read_exfiltration"
…tup fix

PR #99 shipped without CHANGELOG entries. Record the unreleased work:

- πŸ“ docs: lookout, lookout-with-exec, drydock-with-selfupdate presets (12 β†’ 15) + lookout compose example under Added
- πŸ› fix: github-actions-runner / gitlab-runner now load (missing insecure_allow_* flags) under Fixed
scttbnsn added 7 commits June 15, 2026 21:50
* πŸ”„ refactor(presets): rename lookout preset to Portwing

Upstream agent renamed CodesWhat/lookout β†’ CodesWhat/portwing, so the
bundled preset follows. Pure rename, no rule changes.

- πŸ”„ refactor(configs): lookout.yaml β†’ portwing.yaml, lookout-with-exec.yaml β†’ portwing-with-exec.yaml
- πŸ”„ refactor(examples): examples/compose/lookout/ β†’ examples/compose/portwing/
- πŸ“ docs(presets): retitle Lookout sections, update agent URL, fix cross-references
- πŸ“ docs(readme,changelog): update preset list, compose-example list, and v1.4 changelog entry

* 🎨 style(branding): swap in new sockguard logo and regenerate favicons

Replace the logo everywhere it ships and regenerate every derived icon
from the new 1023x1023 source.

- 🎨 style(logo): new sockguard-logo.png at repo root, website/public, docs/public (all three kept byte-identical as before)
- 🎨 style(favicons): regenerate favicon.ico (16/32/48), favicon-96x96.png, apple-touch-icon.png, favicon.svg β€” transparency preserved
- 🎨 style(pwa): regenerate maskable web-app-manifest 192/512 (logo padded to safe zone, flattened on white) to match prior output
- πŸ”§ config(layout): bump icon cache-buster ?v=20260408 β†’ ?v=20260615 so returning visitors refetch

* ✨ feat(banner): new dog startup banner with truecolor rendering

Replace the old ASCII banner with the sockguard dog, and render it in
full 24-bit colour on capable terminals with a clean monochrome fallback.

- 🎨 style(banner): swap the banner art for the block-glyph dog (β–“β–’β–‘β–ˆ), 53 cols
- ✨ feat(banner): embed a 50-col half-block truecolor render (dog_color.ans), shown when the terminal advertises COLORTERM=truecolor/24bit
- πŸ› fix(banner): count runes not bytes in artMaxWidth β€” the block glyphs are 3 bytes each, so len() overcounted ~3x and broke centering
- πŸ”„ refactor(banner): centerArt takes an explicit width arg, so it centers both the rune-measured monochrome art and the ANSI-laden colour art
- πŸ§ͺ test(banner): measure width locally in the test helper instead of mutating package state

* πŸ“¦ deps: refresh dev-tooling + sigstore-go for v1.4 soak

- πŸ“¦ deps(go): bump sigstore/sigstore-go v1.2.0 β†’ v1.2.1 (image-trust path; no behavior change)
- πŸ“¦ deps(npm): Biome 2.4.16 β†’ 2.5.0
- πŸ“¦ deps(npm): Tailwind 4.3.0 β†’ 4.3.1; pin docs in lockstep with website
- πŸ“¦ deps(npm): fumadocs 16.10.0 β†’ 16.10.3, lucide-react 1.17 β†’ 1.18, @radix-ui/react-slot 1.2 β†’ 1.3
- πŸ”’ security(deps): regen lockfile from scratch so the postcss override resolves (npm audit: 0 vulnerabilities)
… failover (#104)

* ✨ feat(upstream): foundation for remote TCP+TLS upstreams with failover

Adds the internal/upstream package (Endpoint, EndpointSpec, Resolver) β€” the
single dial seam that replaces the hardcoded single-unix-socket assumption β€” and
the config schema for it (upstream.endpoints[], upstream.failover, per-endpoint
TLS). Consumers are not yet wired through it; that lands next.

- ✨ feat(upstream): Endpoint + client-TLS-in-dialer, ordered failover Resolver, DOCKER_* env spec
- ✨ feat(config): upstream.endpoints/failover schema + file-free ValidateSpec, register request_timeout default
- πŸ”§ config(reload): upstream.endpoints/failover are reload-immutable; request_timeout stays mutable

* ✨ feat(upstream): wire remote-upstream resolver through every proxy path

Builds on the upstream foundation (6ac99df) by threading the shared
*upstream.Resolver through the whole request stack so failover is coherent
across the proxy, hijack, and side-channel inspects, then ships the docs,
website, and examples for the feature.

- πŸ”„ refactor(proxy): route reverse proxy, exec/attach hijack, ownership,
  visibility, client-ACL, and the filter exec-start inspector through the one
  shared resolver via *WithRoundTripper/*WithDialer constructors; legacy
  single-socket constructors stay as backward-compat wrappers
- πŸ”„ refactor(serve): build the resolver once in newServeRuntime, reuse it
  across hot reloads, start its health loop in the serve lifecycle, and show
  the resolved endpoint label in the banner/startup log
- ✨ feat(upstream): CheckReachable boots a failover set when β‰₯1 endpoint
  answers and fails fast when all are dark; legacy single socket keeps the
  precise not-found/permission fail-fast check
- πŸ› fix(upstream): don't demote the active endpoint on request-scoped errors
  (client cancel / request_timeout) β€” only real reachability failures fail over
- πŸ› fix(upstream): DOCKER_TLS_VERIFY with no DOCKER_CERT_PATH now builds a
  valid system-roots TLS endpoint instead of being rejected as plain TCP
- πŸ› fix(upstream): gate demote's async re-probe to one goroutine per endpoint
  and bind it to the resolver lifetime; serialize setHealth's swap-and-notify
- πŸ› fix(config): reject upstream.failover.health_interval "0s" (ambiguous with
  the default) and point operators at negative-to-disable / omit-for-default
- πŸ› fix(cli): validate header shows configured endpoints, not the unused socket
- πŸ“ docs: new Remote Upstreams & Failover guide, configuration + env-var
  reference, README comparison/roadmap, and a runnable compose example
- πŸ§ͺ test: cover resolver pool tunings, CheckReachable, system-roots TLS,
  request-scoped no-demote, endpoints/failover immutability, and failover env vars
…-scheme

- 🎨 add inverted sockguard-logo-dark.png (force-added past the /*.png ignore, matching the existing logo)

- πŸ“ wrap the README logo in <picture> so it swaps to the dark variant under prefers-color-scheme: dark
… count

- πŸ“ add '## [1.4.0-rc.1] - 2026-06-16' heading under the [Unreleased] stub; release-from-tag.yml's changelog gate requires a dated heading matching the pushed tag

- 🎨 website features.ts: 12 β†’ 15 bundled presets (+ Portwing, Portwing with exec, Drydock with self-update) to match README/CHANGELOG/docs
* 🎨 style(assets): add CodesWhat brand logo (ecosystem footer parity)

* 🎨 style(readme): embed Sigstore + GoReleaser icons (dropped from simple-icons), CodesWhat logo footer

* 🎨 style(readme): trim library badges, add AI-tools row, standards below Built With, ecosystem cross-link table
dev/v1.4 already carried most of the npm-minor group; this folds only
the lagging deltas dependabot opened against frozen main in #102:

- @biomejs/biome ^2.4.16 β†’ ^2.5.0
- turbo ^2.9.17 β†’ ^2.9.18
- fumadocs-core/fumadocs-ui ^16.9.3 β†’ ^16.10.3, fumadocs-mdx ^15.0.11 β†’ ^15.0.12
- lucide-react ^1.17.0 β†’ ^1.18.0
- tailwindcss + @tailwindcss/postcss ^4.3.0 β†’ ^4.3.1 (website)
- @types/node ^25.9.2 β†’ ^25.9.3 (docs + website)

Lockfile regenerated and deduped (clean-env).
…ipped versions

fumadocs from-version (16.9.3 not 16.10.0), drop the @radix-ui/react-slot
1.2β†’1.3 claim (tree is ^1.2.4, never bumped), add the turbo 2.9.17β†’2.9.18
that actually landed via the npm-minor fold (#105).
@vercel

vercel Bot commented Jun 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sockguard-website Ready Ready Preview, Comment Jun 16, 2026 5:00pm

…flow

release-cut auto-computes only stable semver, so rc tags (v1.4.0-rc.1)
fell back to hand-cutting β€” the one rough edge vs the drydock release flow.

- Add an optional `release_tag` workflow_dispatch input (drydock-style):
  when supplied, cut that exact tag (validated format + not-already-exists
  + non-empty CHANGELOG entry); blank still auto-computes the next stable
  version. Fully backward-compatible.
- Fix RELEASING.md: tags are unsigned annotated bot tags (`git tag -a`),
  not signed β€” provenance is cosign keyless + SLSA in release-from-tag,
  not a git-tag signature. Document the rc cut path.
Go Lint was nondeterministic β€” golangci-lint-action ran unpinned, so it
floated to a newer golangci-lint whose staticcheck regressed SA5011 and
false-positived "possible nil pointer dereference" on `if x == nil {
t.Fatal(...) }` guards in serve_reload_test.go / serve_policy_bundle_test.go
(t.Fatal ends the test via Goexit; the deref below is unreachable). Same
SHA passed an earlier run and failed the next. Pin to v2.12.2 β€” the local
lefthook go-lint version β€” so CI lint matches local and is reproducible.
…5011)

CI's golangci-lint flags SA5011 "possible nil pointer dereference" on the
`if x == nil { t.Fatal(...) }` guards in serve_reload_test.go and
serve_policy_bundle_test.go: its staticcheck doesn't model t.Fatal's Goexit
termination, so it thinks the deref on the next line can still see nil. The
local linter (identical v2.12.2 + Go 1.26.4) doesn't flag it, which is what
made Go Lint nondeterministic across environments. An explicit `return`
after each t.Fatal makes the non-nil guarantee explicit in control flow, so
every staticcheck build accepts it. Together with the v2.12.2 pin in
ci-verify, Go Lint is now reproducible local↔CI.
…te returns

Root-cause fix for the nondeterministic Go Lint. CI's golangci-lint binary
flags SA5011 "possible nil pointer dereference" on every
`if x == nil { t.Fatal(...) }; x.field` guard in test files β€” it doesn't
model t.Fatal's runtime.Goexit termination β€” while the identical local
v2.12.2 + Go 1.26.4 reports zero. golangci's max-same-issues meant each run
surfaced only a few sites (serve_reload, serve_policy_bundle, then
imagefetch/bundle_keyless, ...), so the per-site `return` approach from
c07faba was whack-a-mole.

Exclude SA5011 only in `_test.go` (alongside the existing errcheck/gosec/
bodyclose test exclusions) β€” kills the whole false-positive class
deterministically and keeps SA5011 active on production code. Revert the
three band-aid returns now that the class is handled at config level.

@biggest-littlest biggest-littlest left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving the v1.4.0-rc.1 release merge (dev/v1.4 β†’ main). All required CI green; Go Lint now deterministic (SA5011 test-file exclusion) and the request-ID entropy flake cleared on re-run.

@scttbnsn scttbnsn merged commit a444c2d into main Jun 16, 2026
66 of 68 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants